//#define WMB_DEBUG_PRINT

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Mitosoft.Definitions.Data;

// ReSharper disable once CheckNamespace
namespace Mitosoft.TextFileReader 
{
  /// <summary>
  /// Reads lines from large text files implementing a block cache mechanism.
  /// </summary>
  public class TextFileReader : ICacheableDataBlockProvider<IGenericDataBlockItem<string>, string>
  {
    #region delegates and events

    /// <summary>
    /// Method called to get whether the given log line should be considered.
    /// </summary>
    /// <param name="logLineContent">Represents the log line which should be checked whether it should be considered.</param>
    /// <returns>If the first bool value is True it means that the given log line should be considered.
    /// If the second bool value is True it means that the examaning the log file should be finished.</returns>
    public delegate KeyValuePair<bool,bool> ConsiderLogLineDelegate(string logLineContent);

    /// <summary>
    /// Event occuring when checking whether a log line should be considered is requested.
    /// </summary>
    public event ConsiderLogLineDelegate ConsiderTextLineRequest;

    /// <summary>
    /// Occurs when a new instance is requested.
    /// </summary>
    public event CreateDataBlockItemInstanceDelegate DataBlockItemInstanceRequest;

    #endregion

    #region Fields

    protected string _textFileNameIncludingPath = null;
    protected StreamReader _streamReader;
    protected ulong _streamReaderRealPosition = 0;
    protected ulong? _streamReaderLength = null;
    protected string _streamReaderLine = null;
    protected uint _numberOfBytesRepresentingNewLine = 2;
    protected int _currentCharacterBeforeReadingLine;
    protected LinePositionsCache _linePositionsCache;
    protected uint _linePositionsCacheSize = 1024;
    protected ulong? _lineCount = 0;
    protected uint _lineContentCacheSize = 1024;
    protected LineContentCollection _lineContentCache = null;
    protected BinaryReader _binaryReader;
    protected FileStream _fileStream;
    protected ulong? _binaryReaderRealPosition;
    protected ulong? _binaryReaderTextFileLength;
    protected int _sourceIndex;
    #endregion

    #region Constructor(s)

    /// <summary>
    /// Creates a new text file reader instance.
    /// </summary>
    /// <param name="textFileNameIncludingPath">Name including the path of the file containing the text we want to read.</param>
    /// <param name="sourceIndex">Represents the source index.</param>
    public TextFileReader(
      string textFileNameIncludingPath,
      int sourceIndex)
    {
      _sourceIndex = sourceIndex;

      // Check parameter(s):
      if (string.IsNullOrEmpty(textFileNameIncludingPath))
        throw new ArgumentException(
          string.Format(
            "{0} {1}",
            "TextFileReader needs a valid file name",
            "including the path for the text to be read."),
            "textFileNameIncludingPath");

      TextFileNameIncludingPath = textFileNameIncludingPath;
    }

    /// <summary>
    /// Creates a new text file reader instance.
    /// </summary>
    /// <param name="textFileNameIncludingPath">Name including the path of the file containing the text we want to read</param>
    /// <param name="linePositionsCacheSize">See <see cref="LinePositionsCacheSize"/></param>
    /// <param name="sourceIndex">Represents the source index.</param>
    public TextFileReader(
      string textFileNameIncludingPath, 
      uint linePositionsCacheSize,
      int sourceIndex)
    {
      _sourceIndex = sourceIndex;

      // Important: First assign the cache size before assigning the file name
		  LinePositionsCacheSize = linePositionsCacheSize;

	    if( string.IsNullOrEmpty( textFileNameIncludingPath ) )
		    throw new ArgumentException(
			  string.Format(
			    "{0} {1}",
			    "TextFileReader needs a valid file name",
			    "including the path for the text to be read." ),
			    "textFileNameIncludingPath" );

	    TextFileNameIncludingPath = textFileNameIncludingPath;
	  }

    /// <summary>
    /// Creates a new text file reader instance
    /// </summary>
    /// <param name="textFileNameIncludingPath">Name including the path of the file containing the text we want to read</param>
    /// <param name="linePositionsCacheSize">See <see cref="LinePositionsCacheSize"/></param>
    /// <param name="lineContentCacheSize">See <see cref="LineContentCacheSize"/></param>
    /// <param name="sourceIndex">Represents the source index.</param>
    public TextFileReader(
		  string textFileNameIncludingPath, 
		  uint linePositionsCacheSize, 
		  uint lineContentCacheSize,
      int sourceIndex)
    {
      _sourceIndex = sourceIndex;
      LineContentCacheSize = lineContentCacheSize;

	    // Important: First assign the cache size before assigning the file name
	    LinePositionsCacheSize = linePositionsCacheSize;

	    if( string.IsNullOrEmpty( textFileNameIncludingPath ) )
		    throw new ArgumentException(
			  string.Format(
			    "{0} {1}",
			    "TextFileReader needs a valid file name",
			    "including the path for the text to be read." ),
			    "textFileNameIncludingPath" );

	    TextFileNameIncludingPath = textFileNameIncludingPath;
    }
    
    #endregion

    #region Properties

    /// <summary>
    /// Represents the number of lines of the given text file.
    /// </summary>
    public ulong TotalNumberOfRecords
    {
      get { return LineCount; }
    }

    /// <summary>
    /// Reads the line up to the this.BinaryReaderRealPosition
    /// position which have to be set before calling this property.
    /// </summary>
    protected StringBuilder BinaryReaderReadCurrentLine 
	  {
      get
      {
        var stringBuilder = new StringBuilder();
		    Byte lastByte = 0;
        
        // Special case: We are already at EOF:
        if (BinaryReaderTextFileLength != null &&
            BinaryReaderTextFileLength > 0 &&
            BinaryReaderRealPosition >= BinaryReaderTextFileLength)
          return null;

        if (BinaryReaderRealPosition == null)
          _binaryReaderRealPosition = 0;

        do 
        {
          // Check if the file position is equal to the end of the file:
          if (BinaryReaderTextFileLength != null &&
              BinaryReaderTextFileLength > 0 &&
              BinaryReaderRealPosition >= BinaryReaderTextFileLength)
            break;

          Byte currentByte;
          try 
		      {
            currentByte = BinaryReader.ReadByte();
          } 
		      catch (EndOfStreamException) 
		      {
            break;
          } 
		      catch (Exception ex) 
		      {
            throw new ApplicationException(
              string.Format(
                "{0} {1}",
                "Mitosoft.TextFileReader could not read a byte. Error text:",
                ex.Message),
              ex);
          }

          _binaryReaderRealPosition++;  // Dont use the property!
		  
		      if( !(currentByte == 10 || currentByte == 13) && !(lastByte == 10 || lastByte == 13) )
		      {
			      lastByte = currentByte;
			      stringBuilder.Append( (char)currentByte );
			      continue;	// current and last are normal signs, continue reading
		      }

		      if( currentByte == 13 && !(lastByte == 10 || lastByte == 13) )
		      {
			      lastByte = currentByte;
			      continue;	// because CR(13) may be followed by LF(10)
		      }

		      if ( currentByte != 10 && lastByte == 13 )
		      {
			      // We have read one byte too much, it belongs already to the next line
			      BinaryReaderRealPosition = _binaryReaderRealPosition -1;
		      }

	        return stringBuilder;

        } while (true);

        return stringBuilder;
      }
    }

    /// <summary>
    /// Represents the length of the text file.
    /// </summary>
    protected ulong? BinaryReaderTextFileLength 
    {
      get 
      {        
        // Initialize if needed:
        if (_binaryReaderTextFileLength == null) 
        {
          if (BinaryReader == null) 
            return null;
          
          _binaryReaderTextFileLength =
            (ulong)BinaryReader.BaseStream.Length;
        }
        
        return _binaryReaderTextFileLength;
      }

      set { _binaryReaderTextFileLength = value; }
    }

    /// <summary>
    /// Represents the real position inside the current file.
    /// </summary>
    protected ulong? BinaryReaderRealPosition 
    {
      get { return _binaryReaderRealPosition; }
      
      set 
      {
        // Exit if the value is already set:
        if (_binaryReaderRealPosition == value)
          return;
        
        if (_binaryReaderRealPosition == null &&
            value == null)
          return;
        
        // Realize the value if possible:
        if (value != null &&
            value < BinaryReaderTextFileLength)
          BinaryReader.BaseStream.Seek(
            (long) value,
            SeekOrigin.Begin);

        // Save the new value:
        _binaryReaderRealPosition = value;
        
        // Special case: if value is 0, we set 
        // it to null, so we can use comparing
        // the null value to distingish the beginning:
        if (value != null && value == 0)
          _binaryReaderRealPosition = null;
      }
    }

    /// <summary>
    /// Represents the filestream used from the binary reader.
    /// </summary>
    protected FileStream FileStream 
    {
      get 
      {
        if (_fileStream != null) 
          return _fileStream;

        // Initialize if needed:
        if (string.IsNullOrEmpty(TextFileNameIncludingPath))
          return null;
          
        FileStream =
          File.Open(
            TextFileNameIncludingPath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read);

        return _fileStream;
      }

      set { _fileStream = value; }
    }

    /// <summary>
    /// Represents the binary reader used for all file reading operations.
    /// </summary>
    protected BinaryReader BinaryReader 
    {
      get 
      {
        // Initialize if needed:
        if (_binaryReader == null) {
          if (FileStream == null) return null;
          BinaryReader =
            new BinaryReader(FileStream);
        }
        
        return _binaryReader;
      }

      set { _binaryReader = value; }
    }

    /// <summary>
    /// Represents the size of the cache containing the line contents.
    /// </summary>
    protected uint LineContentCacheSize 
    {
      get { return _lineContentCacheSize; }
      set { _lineContentCacheSize = value; }
    }

    /// <summary>
    /// Represents the collection containing the content of lines used as a cache.
    /// </summary>
    protected LineContentCollection LineContentCache 
    {
      get { return _lineContentCache; }
      set { _lineContentCache = value; }
    }

    /// <summary>
    /// Represents the number of lines of the given text file.
    /// </summary>
    protected ulong LineCount 
    {
      get
      {
        // Initialize if needed:
        return _lineCount ?? (ulong)(_lineCount = LineCountFromFile);
      }

      set { _lineCount = value; }
    }

    /// <summary>
    /// Represents the cache size for the cache holding the line positions. 
    /// Setting this value initializes a new line positions cache instance.
    /// Default value is 1024.
    /// </summary>
    protected uint LinePositionsCacheSize 
    {
      get { return _linePositionsCacheSize; }
      set { _linePositionsCacheSize = value; }
    }

    /// <summary>
    /// Represents the cache holding the line positions.
    /// </summary>
    protected LinePositionsCache LinePositionsCache 
    {
      get 
      {
        if (_linePositionsCache == null)
          LinePositionsCache =
            new LinePositionsCache(
              LinePositionsCacheSize);
        
        return _linePositionsCache;
      }

      set { _linePositionsCache = value; }
    }

    /// <summary>
    /// Represents the current available character 
    /// before the line was read. It is used to 
    /// determine the correct real position in the
    /// text file.
    /// </summary>
    protected int CurrentCharacterBeforeReadingLine 
    {
      get { return _currentCharacterBeforeReadingLine; }
      set { _currentCharacterBeforeReadingLine = value; }
    }

    /// <summary>
    /// Represents the number of bytes representing
    /// a new line in the text file. Default is two
    /// bytes which is common for windows text files.
    /// </summary>
    public uint NumberOfBytesRepresentingNewLine 
    {
      get { return _numberOfBytesRepresentingNewLine; }
      set { _numberOfBytesRepresentingNewLine = value; }
    }

    /// <summary>
    /// Represents the last line which was read.
    /// This property should be actualized using
    /// the method this.StreamReaderReadLine().
    /// </summary>
    protected string StreamReaderLine 
    {
      get { return _streamReaderLine; }
      set { _streamReaderLine = value; }
    }

    /// <summary>
    /// Represents the number of bytes of the current file.
    /// We use this property instead of using directly 
    /// this.StreamReader.BaseStream.Length because of 
    /// performance isssues.
    /// </summary>
    protected ulong? StreamReaderLength 
    {
      get { return _streamReaderLength; }
      set { _streamReaderLength = value; }
    }
    
    /// <summary>
    /// Represents the position inside the file we want to read.
    /// This property is actualized when we read a line from the
    /// file and contains the (real) actual position in the file.
    /// </summary>
    protected ulong StreamReaderRealPosition 
    {
      get { return _streamReaderRealPosition; }

      set 
      {
        // Exit if StreamReader is not initialized:
        if (StreamReader == null) return;
        if (StreamReaderLength == null) return;
        
        // Check if the desired position is inside the valid range
        // and correct the value if needed:
        if (value >= StreamReaderLength)
          value = (ulong)(StreamReaderLength - 1);

        // Exit if the value to be set is already set:
        if (StreamReaderRealPosition == value) return;
        
        // Save the new value:
        _streamReaderRealPosition = value;

        // Now realize the real position in the file:
        StreamReader.DiscardBufferedData();
        StreamReader.BaseStream.Seek((long)StreamReaderRealPosition, SeekOrigin.Begin);
      }
    }
   
    /// <summary>
    /// Represents the stream reader used to read the a file.
    /// </summary>
    protected StreamReader StreamReader 
    {
      get { return _streamReader; }
      set { _streamReader = value; }
    }

    /// <summary>
    /// Represents the file name including the path for the text we want to read.
    /// </summary>
    protected string TextFileNameIncludingPath 
    {
      get { return _textFileNameIncludingPath; }

      set 
      {
        // Exit if the value is already set:
        if (value == TextFileNameIncludingPath)
          return;
        
        // Check if given file exists:
        if (!File.Exists(value))
          throw new ArgumentOutOfRangeException(
            "value",
            value,
            string.Format(
              "TextFileReader needs an existing file name " + 
              "including the path for the text to be read."));        
        
        _textFileNameIncludingPath = value;

        Initialize();
      }
    }

    /// <summary>
    /// Retrieves data records.
    /// </summary>
    /// <param name="id">ID of the record we want to start from.</param>
    /// <param name="maximumNumberOfRecords">Maximum number of records we want to retrieve.</param>
    /// <returns>Collection of <see cref="IGenericDataBlockItem{V}"/> values.</returns>
    public IEnumerable<IGenericDataBlockItem<string>> Values(ulong id, ulong maximumNumberOfRecords)
    {
      // Check parameters:
      if (id >= TotalNumberOfRecords || maximumNumberOfRecords == 0)
        yield break;

      ILinePosition startingLinePosition =
        LinePositionsCache.GetNearestLinePositionFromCache(id);

      ulong numberOfRetrievedData = 0;

      BinaryReaderRealPosition = startingLinePosition.PositionInTheFile;

      do
      {
        StringBuilder currentLine = BinaryReaderReadCurrentLine;
        if (currentLine == null) yield break;

        if (startingLinePosition.LineNumber >= id)
        {
          bool considerCurrentLogLine = true;
          bool stopParsingTextFile = false;

          if (ConsiderTextLineRequest != null)
          {
            KeyValuePair<bool, bool> considerCurrentTextLineEvaluationResult =
              ConsiderTextLineRequest(currentLine.ToString());

            considerCurrentLogLine = considerCurrentTextLineEvaluationResult.Key;
            stopParsingTextFile = considerCurrentTextLineEvaluationResult.Value;
          }

          if (considerCurrentLogLine)
          {
            numberOfRetrievedData++;

            IGenericDataBlockItem<string> dataBlockItem =               
              new LineContent(
                currentLine.ToString(),
                startingLinePosition.LineNumber);

            var handler = DataBlockItemInstanceRequest;

            if (handler == null)
              yield return dataBlockItem;
            else
              yield return handler(dataBlockItem, _sourceIndex);
          }

          if (stopParsingTextFile)
            yield break;
        }
        
        if (numberOfRetrievedData >= maximumNumberOfRecords)
          yield break;

        startingLinePosition.LineNumber++;
        startingLinePosition.PositionInTheFile =
          BinaryReaderRealPosition;

      } while (true);
    }

    /// <summary>
    /// Retrieves a given number of lines starting at a given line number.
    /// </summary>
    /// <param name="startingLineNumber">Number of the line we want start reading the contents. 
    /// Set this value to null only the first time you want to read partially the text file, 
    /// for example line for line for evaluation purposes.</param>
    /// <param name="lineNumberToRead">Number of lines we want to be read.
    /// In this number the starting line is included.</param>
    /// <param name="considerLogLineRequest">Delegate used to get whether a log line should be considered.</param>
    /// <returns>Retrieved line content collection or null if nothing was found.</returns>
    public LineContentCollection this[
      ulong? startingLineNumber, 
      uint lineNumberToRead,
      ConsiderLogLineDelegate considerLogLineRequest
      ]
    {
      get
      {
        ConsiderTextLineRequest = considerLogLineRequest;

        // return null if the starting line number is greater then the
        // available number of lines:
        if (startingLineNumber >= LineCount) 
          return null;
        
        // if the number of the required lines is greater then the number of lines 
        // which fits in the line content cache, we retrieve the line contents from the file:
        if (lineNumberToRead > LineContentCacheSize)
          return
            GetLineContentsFromFile(
              startingLineNumber == null ? 0 : (ulong)startingLineNumber,
              lineNumberToRead);

        // Get the line contents from the cache:
        LineContentCollection lineContentsFromCache =
          GetLineContentsFromCache(
            startingLineNumber ?? 0, 
            lineNumberToRead);

        // Return the content of the cache if the requested lines are in the cache:
        if (lineContentsFromCache != null)
          return lineContentsFromCache;
        
        // Correct the starting line number, so it will be in the middle of the cache:
        ulong startingLineNumberToFillTheCache = 
          CacheStartingLineNumber(
            startingLineNumber == null ? 0 : (ulong)startingLineNumber);
        
        // Fill the line content cache with the requested lines from the file:
        LineContentCache =
          GetLineContentsFromFile(startingLineNumberToFillTheCache, LineContentCacheSize);

        // Now the line content cache is filled, so we can retrieve the required lines from the cache:
        return GetLineContentsFromCache(startingLineNumber,lineNumberToRead);
      }
    }
       
    /// <summary>
    /// Represents the line number of the first line in the line content cache. 
    /// If the line content cache is not defined or it is empty, 
    /// null will be returned.
    /// </summary>
    protected ulong? LineNumberOfFirstLineInLineContentCache {
      get 
      {
        if (LineContentCache == null || LineContentCache.Count == 0)
          return null;

        return LineContentCache[0].LineNumber;
      }
    }
    
    /// <summary>
    /// Represents the line number of the last line in the line content cache. 
    /// If the line content cache is not defined or it is empty, 
    /// null will be returned.
    /// </summary>
    protected ulong? LineNumberOfLastLineInLineContentCache {
      get 
      {
        if (LineContentCache == null || LineContentCache.Count == 0)
          return null;

        return LineContentCache[LineContentCache.Count - 1].LineNumber;
      }
    }
    
    /// <summary>
    /// Retrieves the number of lines by reading all lines from the text file.
    /// </summary>
    protected ulong LineCountFromFile 
    {
      get 
      {
        if (StreamReader == null) return 0;

        ulong currentLineNumber = 0;

        // Position at the begin of the file:
        StreamReader.DiscardBufferedData();
        StreamReader.BaseStream.Seek(0, SeekOrigin.Begin);
        
        // Parse all text lines
        do 
        {
          string currentLineContent = StreamReader.ReadLine();
          if (currentLineContent == null) break;
          currentLineNumber++;
        } while (true);

        return currentLineNumber;
      }
    }

    #endregion

    #region Methods

    /// <summary>
    /// Returns the first line number to be read in the cache.
    /// Ideally the given current line number will be in the middle of the 
    /// cache lines. Special cases are the beginning and the end of the file.
    /// </summary>
    /// <param name="currentLineNumber">Current line number.</param>
    /// <returns>Calculated number of the first line in the cache.</returns>
    protected ulong CacheStartingLineNumber(ulong currentLineNumber)
    {
      var resultingStartingLineNumber =
        (ulong)Math.Max(0, (long)currentLineNumber - (long)LineContentCacheSize / 2);

      if (resultingStartingLineNumber + LineContentCacheSize > LineCount)
        resultingStartingLineNumber = 
          (ulong)Math.Max(0, (long)LineCount - LineContentCacheSize);

      return resultingStartingLineNumber;
    }
    
    /// <summary>
    /// Retrieves a given number of content lines from the file starting at a given line number.
    /// </summary>
    /// <param name="startingLineNumber">Number of the line inside the content file we want start reading the content.</param>
    /// <param name="lineNumberToRead">Number of lines, including the starting line, we want to be read.</param>
    /// <returns>Retrieved line content collection or null if nothing was found.</returns>
    protected LineContentCollection GetLineContentsFromFile(
      ulong startingLineNumber, uint lineNumberToRead ) 
    {
      #if WMB_DEBUG_PRINT
        Debug.Print("Get content from file: Line# {0}, Count={1}", startingLineNumber, lineNumberToRead);
      #endif

      // Check parameters:
      if (startingLineNumber >= LineCount || lineNumberToRead == 0)
        return null;
        
      ILinePosition startingLinePosition =
        LinePositionsCache.GetNearestLinePositionFromCache(startingLineNumber);
        
      BinaryReaderRealPosition = startingLinePosition.PositionInTheFile;
        
      var readLines = new LineContentCollection();

      do 
        {
          StringBuilder currentLine = BinaryReaderReadCurrentLine;
          if (currentLine == null) break;

          if (startingLinePosition.LineNumber >= startingLineNumber)
          {
            var considerCurrentLogLine = true;
            var stopParsingTextFile = false;

            if (ConsiderTextLineRequest != null)
            {
              KeyValuePair<bool, bool> considerCurrentTextLineEvaluationResult = 
                ConsiderTextLineRequest(currentLine.ToString());

              considerCurrentLogLine = considerCurrentTextLineEvaluationResult.Key;
              stopParsingTextFile = considerCurrentTextLineEvaluationResult.Value;
            }

            if (considerCurrentLogLine)
            {
              ILineContent foundLineContent = 
                new LineContent(
                  currentLine.ToString(),
                  startingLinePosition.LineNumber);

              readLines.Add(foundLineContent);
            }

            if (stopParsingTextFile)
              break;
          }
          
          if (readLines.Count >= lineNumberToRead) 
            break;

          startingLinePosition.LineNumber++;
          startingLinePosition.PositionInTheFile =
            BinaryReaderRealPosition;
            
        } while (true);
        
        return readLines.Count == 0 ? null : readLines;
    }
    
    /// <summary>
    /// Retrieves the line contents from the line content cache. 
    /// If the retrieved range is completely contained in the line content cache, 
    /// the required line contents will be returned. 
    /// If the required range is not completely covered by the line content cache, 
    /// null will be returned.
    /// </summary>
    /// <param name="startingLineNumber">Number of the line we want start reading the contents.</param>
    /// <param name="lineNumberToRead">Number of the lines we want to be read. 
    /// In this number the starting line is included.</param>
    /// <returns>Retrieved line content collection or null if the required line content range is not completely
    /// contained in the line content cache.</returns>
    protected LineContentCollection GetLineContentsFromCache(
      ulong? startingLineNumber, uint lineNumberToRead)
    {
      // Check parameters:
      if (lineNumberToRead == 0)
        return null;

      var correctedStartingLineNumber =
        startingLineNumber != null ? 
          (ulong)startingLineNumber : 
          ( LineNumberOfFirstLineInLineContentCache == null ? 
              0 : 
              (ulong)LineNumberOfFirstLineInLineContentCache);

      ulong? lineNumberOfFirstLineInLineContentCache = 
        LineNumberOfFirstLineInLineContentCache;
      ulong? lineNumberOfLastLineInLineContentCache = 
        LineNumberOfLastLineInLineContentCache;

      if (lineNumberOfFirstLineInLineContentCache == null ||
          lineNumberOfLastLineInLineContentCache == null) 
        return null;
      
      // Return null if the requested range is not contained
      // completely in the cache:
      if (correctedStartingLineNumber < 
            lineNumberOfFirstLineInLineContentCache     ||
          correctedStartingLineNumber > 
            lineNumberOfLastLineInLineContentCache      ||
          correctedStartingLineNumber + lineNumberToRead - 1 < 
            lineNumberOfFirstLineInLineContentCache     ||
          correctedStartingLineNumber + lineNumberToRead - 1 >
            lineNumberOfLastLineInLineContentCache)
        return null;

      // Get now the line contents from the line content cache:
      uint? startingIndexInLineContentCache =
        IndexOfLineContentInsideLineContentCache(correctedStartingLineNumber);

      if (startingIndexInLineContentCache == null)
        throw new ApplicationException(
          string.Format(
            "{0} {1} {2} {3}",
            "Unlogical state in method GetLineContentsFromCache(). The starting index of the line number",
            correctedStartingLineNumber, "could not be found although the requested range is completely",
            "covered by the line content cache!?"));
      
      var lineContentsFromCache = new LineContentCollection();

      for(uint i=0; i<lineNumberToRead; i++) 
      {
        ILineContent currentLineContentInCache = 
          LineContentCache[(int)(startingIndexInLineContentCache + i)];

        ILineContent foundLineContent = 
          new LineContent(
            currentLineContentInCache.Content,
            currentLineContentInCache.LineNumber);

        lineContentsFromCache.Add(foundLineContent);
      }

      return lineContentsFromCache;
    }

    /// <summary>
    /// Retrieves the index of the line content in the line content cache.
    /// </summary>
    /// <param name="lineNumber">Identifies the line</param>
    /// <returns>Found index or null if nothing was found</returns>
    protected uint? IndexOfLineContentInsideLineContentCache(ulong lineNumber) 
    {
      // Return null if cache is undefined or empty:
      if (LineContentCache == null || LineContentCache.Count == 0)
        return null;
      
      for(uint i=0; i < LineContentCache.Count; i++)
        if (LineContentCache[(int)i].LineNumber == lineNumber)
          return i;
      
      return null;  // Nothing found
    }

    /// <summary>
    /// Fills the line positions cache.
    /// </summary>
    protected void LinePositionsCacheFill() 
    {
      #if WMB_DEBUG_PRINT
        Debug.Print("**** Start filling line positions cache.");
      #endif
      
      // Divide the total number of lines in the file by the available 
      // number of positions to know when we have to save a position:
      var saveEveryNPosition = 
        (uint) ((long)LineCount / ((long)LinePositionsCacheSize + 1));

      if (saveEveryNPosition == 0) saveEveryNPosition++;
      
      // Position at the file begin:
      BinaryReaderRealPosition = 0;

      // Define and initialize local variables:
      uint positionCounter = 0;
      ILinePosition linePosition = new LinePosition(0,0);

      Byte lastByte = 0;

      do 
	    {
        // Check if the file position is equal to
        // the end of the file:
        if (BinaryReaderTextFileLength != null &&
            BinaryReaderTextFileLength > 0 &&
            BinaryReaderRealPosition >= BinaryReaderTextFileLength -1)
          break;

        if (BinaryReaderRealPosition == null)
          _binaryReaderRealPosition = 0;

		    Byte currentByte;

	      try
	      {
	        currentByte = BinaryReader.ReadByte();
	      }
	      catch (EndOfStreamException)
	      {
	        break;
	      } 
		    catch(Exception ex) 
		    {
          throw new ApplicationException(
            string.Format(
              "Mitosoft.TextFileReader could not read a " +
              "byte. Error text: {0}",
              ex.Message),
            ex);
        }

        _binaryReaderRealPosition++;    // Dont use the property!

		    if( !(currentByte == 10 || currentByte == 13) && !(lastByte == 10 || lastByte == 13) )
		    {
			    lastByte = currentByte;
			    continue;	// current and last are normal signs, continue reading
		    }

		    if (currentByte == 13 && !(lastByte == 10 || lastByte == 13))
		    {
			    lastByte = currentByte;
			    continue;	// because CR(13) may be followed by LF(10)
		    }

		    if (currentByte == 13 && lastByte == 13)
			    linePosition.LineNumber++;	// extra line number increment

		    lastByte = 0;
		    linePosition.LineNumber++;

        if (++positionCounter != saveEveryNPosition)
			    continue;
        
        positionCounter = 0;
        linePosition.PositionInTheFile = 
          BinaryReaderRealPosition;
        
        // Save line position in the cache:
        LinePositionsCache.SaveNewLinePosition(linePosition);

        if (LinePositionsCache.CacheIsFull)
          break;
        
      } while (true);

      #if WMB_DEBUG_PRINT
        Debug.Print("**** End filling line positions cache.");
      #endif
    }

    /// <summary>
    /// Reads the next text line and actualizes the current position. 
    /// The result is stored in this.StreamReaderLine.
    /// </summary>
    protected void StreamReaderReadLine() {
      
      CurrentCharacterBeforeReadingLine = StreamReader.Peek();

      StreamReaderLine = StreamReader.ReadLine();
      StreamReaderPositionsActualize();
    }

    /// <summary>
    /// Actualizes the internal property this.StreamReaderRealPosition and this.StreamReaderPosition.
    /// </summary>
    protected void StreamReaderPositionsActualize() {

      // Dont use here the property this.StreamReaderRealPosition!
      // because we dont want the set - logic to be run

      #region special cases
      
      if (StreamReader == null || StreamReaderLength == null) {
        _streamReaderRealPosition = 0;
        return;
      }

      #endregion

      #region Calculate the real position
      
      ulong newPossibleStreamReaderRealPosition =
        _streamReaderRealPosition +
          (StreamReaderLine == null ? 
              0 : 
              (ulong)StreamReaderLine.Length +
                NumberOfBytesRepresentingNewLine);
      
      // Special case: When we have two bytes representing a new line
      // and we read the ASCII code 10 before reading the line,
      // we correct the position by decreasing it by one.
      // This is because we ASSUME that the bytes representing the
      // new line consists FIRST of the ASCII code 13 and THEN
      // of the ASCII code 10:
      if (StreamReaderLine != null &&
          StreamReaderLine.Length == 0 &&
          NumberOfBytesRepresentingNewLine == 2 && 
          CurrentCharacterBeforeReadingLine == 10)
        newPossibleStreamReaderRealPosition--;

      _streamReaderRealPosition =
        newPossibleStreamReaderRealPosition >= (ulong)StreamReaderLength ?
          (ulong)StreamReaderLength - 1    :
          newPossibleStreamReaderRealPosition;
      
      #endregion

    }
    
    /// <summary>
    /// Actualizes the internal property this.StreamReaderLength
    /// and will be set to -1 if this.StreamReader is null.
    /// </summary>
    protected void StreamReaderLengthActualize() {

      StreamReaderLength =
        (StreamReader == null ?
           null :
           (ulong?)StreamReader.BaseStream.Length);
    }

    /// <summary>
    /// Initializes the internal objects StreamReader, ...
    /// </summary>
    protected void Initialize() 
    {
        StreamReader = File.OpenText(TextFileNameIncludingPath);
        StreamReaderLengthActualize();
        StreamReaderPositionsActualize();
        
        // Setting the following values to null will lead
        // to be initialized when the values are read:
        _lineCount = null;
        FileStream = null;
        BinaryReader = null;
        BinaryReaderTextFileLength = null;

        LinePositionsCacheFill();
    }
    
    #endregion
  }
  
}
